package com.dbflow5.query;


import com.dbflow5.query.property.IProperty;
import com.dbflow5.query.property.Property;
import com.dbflow5.query.property.PropertyFactory;
import com.dbflow5.sql.SQLiteType;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Represents SQLite methods on columns. These act as [Property] so we can use them in complex
 * scenarios.
 */
public class Method extends Property<Object> {

    private final List<IProperty<?>> propertyList = new ArrayList<>();
    private final List<String> operationsList = new ArrayList<>();
    private IProperty<?> methodProperty;
    private NameAlias _nameAlias = null;

    public Method(String methodName, IProperty<?>... properties) {
        super(null, "");

        methodProperty = new Property<>(null, NameAlias.rawBuilder(methodName).build());

        if (properties == null || properties.length == 0) {
            propertyList.add(Property.ALL_PROPERTY);
        } else {
            for(IProperty<?> property : properties) {
                addProperty(property);
            }
        }
    }

    public Method(IProperty<?>... properties) {
        this("", properties);
    }

    @Override
    public Method plus(IProperty<?> property) {
        return append(property, " "+Operator.Operation.PLUS);
    }

    @Override
    public Method minus(IProperty<?> property) {
        return append(property, " "+Operator.Operation.MINUS);
    }

    @Override
    public Property<Object> div(IProperty<?> property) {
        return append(property, " "+Operator.Operation.DIVISION);
    }

    @Override
    public Property<Object> times(IProperty<?> property) {
        return append(property, " "+Operator.Operation.MULTIPLY);
    }

    @Override
    public Property<Object> rem(IProperty<?> property) {
        return append(property, " "+Operator.Operation.MOD);
    }

    /**
     * Allows adding a property to the [Method]. Will remove the [Property.ALL_PROPERTY]
     * if it exists as first item.
     *
     * @param property The property to add.
     * @return Method
     */
    public Method addProperty(IProperty<?> property) {
        return append(property, ",");
    }

    /**
     * Appends a property with the specified operation that separates it. The operation will appear before
     * the property specified.
     * @param property property
     * @param operation operation
     * @return Method
     */
    public Method append(IProperty<?> property, String operation) {
        // remove all property since its not needed when we specify a property.
        if (propertyList.size() == 1 && propertyList.get(0) == Property.ALL_PROPERTY) {
            propertyList.remove(0);
        }
        propertyList.add(property);
        operationsList.add(operation);
        return this;
    }

    protected List<IProperty<?>> getPropertyList() {
        return propertyList;
    }

    @Override
    public NameAlias nameAlias() {
        NameAlias alias = _nameAlias;
        if (alias == null) {
            StringBuilder query = new StringBuilder(methodProperty.getQuery() != null? methodProperty.getQuery() : "");
            query.append("(");
            List<IProperty<?>> propertyList = getPropertyList();
            for (int i=0;i<propertyList.size();i++) {
                IProperty<?> property = propertyList.get(i);
                if (i > 0) {
                    query.append(operationsList.get(i));
                    query.append(" ");
                }
                query.append(property.toString());
            }
            query.append(")");
            alias = NameAlias.rawBuilder(query.toString()).build();
        }
        this._nameAlias = alias;
        return alias;
    }

    /**
     * Represents the SQLite CAST operator.
     */
    public static class Cast{
        private IProperty<?> property;

        Cast(IProperty<?> property) {
            this.property = property;
        }

        /**
         * A new [Method] that represents the statement.
         * @param sqLiteType The type of column to cast it to.
         * @return A new [Method] that represents the statement.
         */
        public Property<Object> as(SQLiteType sqLiteType) {
            Property<Object> newProperty = new Property<>(property.table(),
                property.nameAlias()
                    .newBuilder()
                    .shouldAddIdentifierToAliasName(false)
                    .as(sqLiteType.name())
                    .build());
            return new Method("CAST", newProperty);
        }

        /**
         * Returns a [Property] of [Int] so it can accept [Int] values.
         * @return Property
         */
        public Property<Integer> asInteger() {
            return (Property)(as(SQLiteType.INTEGER));
        }

        /**
         * Returns a [Property] of [Double] so it can accept [Double] values.
         * @return Property
         */
        public Property<Double> asReal() {
            return (Property)as(SQLiteType.REAL);
        }

        /**
         * Returns a [Property] of [String] so it can accept [String] values.
         * @return Property
         */
        public Property<String> asText() {
            return (Property)as(SQLiteType.TEXT);
        }
    }

    /**
     * The average value of all properties within this group.
     * @param properties Set of properties that the method acts on.
     * @return The average value of all properties within this group. The result is always a float from this statement
     * as long as there is at least one non-NULL input. The result may be NULL if there are no non-NULL columns.
     */
    public static Method avg(IProperty<?>... properties) {
        return new Method("AVG", properties);
    }

    /**
     * A count of the number of times that specified properties are not NULL in a group.
     * @param properties Set of properties that the method acts on.
     * @return A count of the number of times that specified properties are not NULL in a group. Leaving
     * the properties empty returns COUNT(*), which is the total number of rows in the query.
     */
    public static Method count(IProperty<?>... properties) {
        return new Method("COUNT", properties);
    }

    /**
     * A string which is the concatenation of all non-NULL values of the properties.
     * @param properties Set of properties that the method acts on.
     * @return A string which is the concatenation of all non-NULL values of the properties.
     */
    public static Method groupConcat(IProperty<?>... properties) {
        return new Method("GROUP_CONCAT", properties);
    }

    /**
     * The method that represents the max of the specified columns/properties.
     * @param properties Set of properties that the method acts on.
     * @return The method that represents the max of the specified columns/properties.
     */
    public static Method max(IProperty<?>... properties) {
        return new Method("MAX", properties);
    }

    /**
     * The method that represents the min of the specified columns/properties.
     * @param properties Set of properties that the method acts on.
     * @return The method that represents the min of the specified columns/properties.
     */
    public static Method min(IProperty<?>... properties) {
        return new Method("MIN", properties);
    }

    /**
     * The method that represents the sum of the specified columns/properties.
     * @param properties Set of properties that the method acts on.
     * @return The method that represents the sum of the specified columns/properties.
     */
    public static Method sum(IProperty<?>... properties) {
        return new Method("SUM", properties);
    }

    /**
     * The method that represents the total of the specified columns/properties.
     * @param properties Set of properties that the method acts on.
     * @return The method that represents the total of the specified columns/properties.
     */
    public static Method total(IProperty<?>... properties) {
        return new Method("TOTAL", properties);
    }

    /**
     * A new CAST object.
     * @param property The property to cast.
     * @return A new CAST object. To complete use the [Cast. as] method.
     */
    public static Method.Cast cast(IProperty<?> property) {
        return new Cast(property);
    }

    public static Method replace(IProperty<?> property, String findString, String replacement) {
        return new Method("REPLACE", property, PropertyFactory.from(findString), PropertyFactory.from(replacement));
    }

    /**
     * SQLite standard "strftime()" method. See SQLite documentation on this method.
     * @param formatString formatString
     * @param timeString timeString
     * @param modifiers modifiers
     * @return this
     */
    public static Method strftime(String formatString, String timeString, String... modifiers) {
        List<IProperty<?>> propertyList = new ArrayList<>();
        propertyList.add(PropertyFactory.from(formatString));
        propertyList.add(PropertyFactory.from(timeString));
        for(String modifier : modifiers) {
            propertyList.add(PropertyFactory.from(modifier));
        }
        return new Method("strftime", propertyList.toArray(new IProperty<?>[]{}));
    }

    /**
     * Sqlite "datetime" method. See SQLite documentation on this method.
     * @param timeStamp timeStamp
     * @param modifiers modifiers
     * @return this
     */
    public static Method datetime(long timeStamp, String... modifiers) {
        List<IProperty<?>> propertyList = new ArrayList<>();
        propertyList.add(PropertyFactory.from(timeStamp));
        for(String modifier : modifiers) {
            propertyList.add(PropertyFactory.from(modifier));
        }
        return new Method("datetime", propertyList.toArray(new IProperty[]{}));
    }

    /**
     * Sqlite "date" method. See SQLite documentation on this method.
     * @param timeString timeString
     * @param modifiers modifiers
     * @return this
     */
    public static Method date(String timeString, String... modifiers) {
        List<IProperty<?>> propertyList = new ArrayList<>();
        propertyList.add(PropertyFactory.from(timeString));
        for(String modifier : modifiers) {
            propertyList.add(PropertyFactory.from(modifier));
        }
        return new Method("date", propertyList.toArray(new IProperty<?>[]{}));
    }

    /**
     * Constructs using the "IFNULL" method in SQLite.
     * @param first first
     * @param secondIfFirstNull secondIfFirstNull
     * @return Constructs using the "IFNULL" method in SQLite. It will pick the first non null
     * value and set that. If both are NULL then it will use NULL.
     */
    public static Method ifNull(IProperty<?> first, IProperty<?> secondIfFirstNull) {
        return new Method("IFNULL", first, secondIfFirstNull);
    }

    /**
     * Constructs using the "NULLIF" method in SQLite.
     * @param first first
     * @param second second
     * @return Constructs using the "NULLIF" method in SQLite. If both expressions are equal, then
     * NULL is set into the DB.
     */
    public static Method nullIf(IProperty<?> first, IProperty<?> second) {
        return new Method("NULLIF", first, second);
    }

    public static Method random() {
        return new Method("RANDOM", Property.NO_PROPERTY);
    }

    /**
     * Used for FTS:
     *
     * For a SELECT query that uses the full-text index, the offsets() function returns a
     * text value containing a series of space-separated integers. For each term in each phrase
     * match of the current row, there are four integers in the returned list.
     * Each set of four integers is interpreted as follows:
     * Integer	Interpretation
     *  0	The column number that the term instance occurs in (0 for the leftmost column of the FTS table, 1 for the next leftmost, etc.).
     *  1	The term number of the matching term within the full-text query expression. Terms within a query expression are numbered starting from 0 in the order that they occur.
     *  2	The byte offset of the matching term within the column.
     *  3	The size of the matching term in bytes.
     *
     *  For more see sqlite.org
     *
     * @param table table
     * @param <T> <T>
     * @return Method
     */
    public static <T> Method offsets(Class<T> table) {
        return new Method("offsets", PropertyFactory.tableName(table));
    }

    /**
     * Used for FTS:
     * The snippet function is used to create formatted fragments of document text for
     * display as part of a full-text query results report.
     *
     * @param table table
     * @param start - the start match text.
     * @param end - the end match text
     * @param ellipses ellipses
     * @param index - The FTS table column number to extract the returned fragments of
     * text from. Columns are numbered from left to right starting with zero.
     * A negative value indicates that the text may be extracted from any column.
     * @param approximateTokens - The absolute value of this integer argument is used as the
     * (approximate) number of tokens to include in the returned text value.
     * The maximum allowable absolute value is 64.
     * @return Method
     * For more see sqlite.org
     */
    public static <T> Method snippet(Class<T> table, String start, String end, String ellipses, int index, int approximateTokens) {
        List<Object> list = Arrays.asList(PropertyFactory.tableName(table), start, end, ellipses, index, approximateTokens);
        List<Property<?>> propertyList = new ArrayList<>();
        for(Object obj : list) {
            if(obj instanceof String){
                propertyList.add(PropertyFactory.propertyString(table, "'"+obj+"'"));
            }else {
                propertyList.add(PropertyFactory.propertyString(table, obj.toString()));
            }
        }
        return new Method("snippet", propertyList.toArray(new IProperty[]{}));
    }
}

